<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>4.实战（五）：实现3D地球可视化（下）-摆放地标</title>
    <style>
        #container{
            border:1px solid #ccc;
            width:512px;
            height:512px;
        }
    </style>
</head>
<body>
    <div id="container"></div>

    <!-- 解压topojson的工具 -->
    <script src="https://lib.baomitu.com/topojson/3.0.2/topojson.min.js"></script>

    <script src="http://unpkg.com/spritejs/dist/spritejs.js"></script>
    <!-- spritejs的3d扩展可以实现3d图形 -->
    <script src="http://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.js"></script>
    
    <script src="https://d3js.org/d3-array.v2.min.js"></script>
    <!-- 地图投影工具方法 -->
    <script src="https://d3js.org/d3-geo.v2.min.js"></script>


    <script type="module">
        // spritejs的3d扩展可以实现3d图形的一般方法
        // 1.加载js

        // 2.创建场景，添加layer对象
        const { Scene } = spritejs;
        const container = document.getElementById("container");
        const scene = new Scene({
            container
        });
        // 与 2D 的 Layer 不同，SpriteJS 的 3D 扩展创建的 Layer 需要设置相机
        const layer = scene.layer3d("fglayer",{
            alpha: false,
            // 初始化透视相机的视角与位置
            camera: { 
                fov: 35, 
                pos: [0, 0, 5], 
            },
        });

        // 2.创建webgl program
        // SpriteJS 的 3D 扩展内置了一些常用的 Shader。以及一些几何体，这里用的是Sphere球体
        const { Sphere, Cylindar } = spritejs.ext3d;

        // 4.创建投影
        // 先绘制一张平面地图，然后通过纹理的方式添加到地球上，作为纹理的球面地图需要用等角方位投影（d3-geo模块支持）
        const mapWidth = 960;
        const mapHeight = 480;
        const mapScale = 4;
        // 通过d3.geoEquirectangular方法创建等角方位投影
        const projection = d3.geoEquirectangular();
        projection
        // 再将这个进行缩放，d3的地图投影默认宽高960*480，将投影缩放为4倍，也就是将地图绘制为3480 * 1920 大小，这样就能在大屏上显示的更清晰
        .scale(projection.scale() * mapScale)
        // 将中心点调整到画布中心，因为 JSON 的地图数据的 0,0 点在画布正中心
        // 你会注意到我们在 Y 方向上多调整一个像素，这是因为原始数据坐标有一点偏差。
        .translate([mapWidth * mapScale * 0.5, (mapHeight + 2) * mapScale * 0.5]);

        // 5.绘制地图
        async function loadMap(src = topojsonData, {strokeColor, fillColor} = {}) {
            const data = await (await fetch(src)).json();
            const countries = topojson.feature(data, data.objects.countries);
            // 先将地图数据绘制到离屏canvas上
            const canvas = new OffscreenCanvas(mapScale * mapWidth, mapScale * mapHeight);
            const context = canvas.getContext('2d');
            context.imageSmoothingEnabled = false;
            return drawMap({context, countries, strokeColor, fillColor});
        }
        
        function drawMap({
            context,
            countries,
            strokeColor = '#666',
            fillColor = '#000',
            strokeWidth = 1.5,
        } = {}) {
            const path = d3.geoPath(projection).context(context);
            context.save();
            context.strokeStyle = strokeColor;
            context.lineWidth = strokeWidth;
            context.fillStyle = fillColor;
            context.beginPath();
            path(countries);
            context.fill();
            context.stroke();
            context.restore();

            coordinate(countries)
            return context.canvas;
        }

        // 6.将地图作为纹理
        const vertex = `
            precision highp float;
            precision highp int;

            attribute vec3 position;
            attribute vec3 normal;
            attribute vec4 color;
            attribute vec2 uv;

            uniform mat4 modelViewMatrix;
            uniform mat4 projectionMatrix;
            uniform mat3 normalMatrix;

            varying vec3 vNormal;
            varying vec2 vUv;
            varying vec4 vColor;

            uniform vec3 pointLightPosition; //点光源位置

            void main() {
                vNormal = normalize(normalMatrix * normal);

                vUv = uv;
                vColor = color;

                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }    
        `;
        const fragment = `
            precision highp float;
            precision highp int;

            varying vec3 vNormal;
            varying vec4 vColor;

            uniform sampler2D tMap;
            varying vec2 vUv;

            uniform vec2 uResolution;

            void main() {
                vec4 color = vColor;
                vec4 texColor = texture2D(tMap, vUv);
                vec2 st = gl_FragCoord.xy / uResolution;

                float alpha = texColor.a;
                color.rgb = mix(color.rgb, texColor.rgb, alpha);
                color.rgb = mix(texColor.rgb, color.rgb, clamp(color.a / max(0.0001, texColor.a), 0.0, 1.0));
                color.a = texColor.a + (1.0 - texColor.a) * color.a;

                float d = distance(st, vec2(0.5));

                gl_FragColor.rgb = color.rgb + 0.3 * pow((1.0 - d), 3.0);
                gl_FragColor.a = color.a;
            } 
        `;
        // 星空背景的shaders
        const skyVertex = `
            precision highp float;
            precision highp int;

            attribute vec3 position;
            attribute vec3 normal;
            attribute vec2 uv;

            uniform mat3 normalMatrix;
            uniform mat4 modelViewMatrix;
            uniform mat4 projectionMatrix;

            varying vec2 vUv;

            void main() {
                vUv = uv;
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }
        `;
        const skyFragment = `
            precision highp float;
            precision highp int;
            varying vec2 vUv;

            highp float random(vec2 co)
            {
                highp float a = 12.9898;
                highp float b = 78.233;
                highp float c = 43758.5453;
                highp float dt= dot(co.xy ,vec2(a,b));
                highp float sn= mod(dt,3.14);
                return fract(sin(sn) * c);
            }

            // Value Noise by Inigo Quilez - iq/2013
            // https://www.shadertoy.com/view/lsf3WH
            highp float noise(vec2 st) {
                vec2 i = floor(st);
                vec2 f = fract(st);
                vec2 u = f * f * (3.0 - 2.0 * f);
                return mix( mix( random( i + vec2(0.0,0.0) ),
                                random( i + vec2(1.0,0.0) ), u.x),
                            mix( random( i + vec2(0.0,1.0) ),
                                random( i + vec2(1.0,1.0) ), u.x), u.y);
            }

            void main() {
                gl_FragColor.rgb = vec3(1.0);
                gl_FragColor.a = step(0.93, noise(vUv * 6000.0));
            }
        `

        const texture = layer.createTexture({});
        let imgCache = null;
        loadMap("./world-topojson.json")
        .then((map) => { 
            texture.image = map; 
            texture.needsUpdate = true; 
            layer.forceUpdate();
        });
        const program = layer.createProgram({
            vertex,
            fragment,
            texture,
            cullFace: null,
        });
        
        // 实现星空背景
        let skyBox = null;
        function createSky(layer, skyProgram) {
            skyProgram = skyProgram || layer.createProgram({
                vertex: skyVertex,
                fragment: skyFragment,
                transparent: true,
                cullFace: null,
            });
            skyBox = new Sphere(skyProgram);
            skyBox.attributes.scale = 100;
            layer.append(skyBox);
            return skyBox;
        }

        createSky(layer);

        // 3.创建球体
        const globe = new Sphere(program, {
            colors: '#333',  //颜色
            widthSegments: 64,  //设置宽度
            heightSegments: 32,  //设置高度
            radius: 1, //半径
        });

        // 渲染球体
        layer.append(globe);

        // 7.坐标转换，鼠标交互
        function coordinate(countries){
            layer.setRaycast();
        
            // 鼠标在地球上移动的时候，通过 SpriteJS，我们拿到三维的球面坐标
            globe.addEventListener('mousemove', (e) => {
                const [lng, lat] = positionToLatlng(...e.hit.localPoint);
                const country = getCountryInfo(lat, lng, countries);
                if(country.properties) {
                    console.log(country.properties.name);
                    highlightMap(texture, country, countries)
                }
            });
            // 将天空包围盒的 raycast 设置成了 none
            skyBox.attributes.raycast = 'none';

            
            /**
             * 将球面坐标转换为平面地图坐标
             * @param {*} x
             * @param {*} y
             * @param {*} z
             * @param {*} radius
             */
            function unproject(x, y, z, radius = 1) {
                const pLength = Math.PI * 2;
                const tLength = Math.PI;
                const v = Math.acos(y / radius) / tLength; // const y = radius * Math.cos(v * tLength);
                let u = Math.atan2(-z, x) + Math.PI; // z / x = -1 * Math.tan(u * pLength);
                u /= pLength;
                return [u * mapScale * mapWidth, v * mapScale * mapHeight];
            }

            // 通过等角方位投影的反函数，将平面直角坐标转换为经纬度
            function positionToLatlng(x, y, z, radius = 1) {
                const [u, v] = unproject(x, y, z, radius);
                return projection.invert([u, v]);
            }

            // 通过经纬度拿到国家信息
            function getCountryInfo(latitude, longitude, countries) {
                if(!countries) return {index: -1};
                let idx = -1;
                countries.features.some((d, i) => {
                    const ret = d3.geoContains(d, [longitude, latitude]);
                    if(ret) idx = i;
                    return ret;
                });
                const info = idx >= 0 ? {...countries.features[idx]} : {};
                info.index = idx;
                return info;
            }
        }

        // 8.高亮国家地区
        function highlightMap(texture, info, countries) {
            if(texture.index === info.index) return;
            const canvas = texture.image;
            if(!canvas) return;

            const idx = info.index;
            const highlightMapContxt = canvas.getContext('2d');

            if(!imgCache) {
                imgCache = new OffscreenCanvas(canvas.width, canvas.height);
                imgCache.getContext('2d').drawImage(canvas, 0, 0);
            }
            highlightMapContxt.clearRect(0, 0, mapScale * mapWidth, mapScale * mapHeight);
            highlightMapContxt.drawImage(imgCache, 0, 0);

            if(idx > 0) {
                const path = d3.geoPath(projection).context(highlightMapContxt);
                highlightMapContxt.save();
                highlightMapContxt.fillStyle = '#fff';
                highlightMapContxt.beginPath();
                // 只绘制高亮区域
                path({type: 'FeatureCollection', features: countries.features.slice(idx, idx + 1)});
                highlightMapContxt.fill();
                highlightMapContxt.restore();
            }
            texture.index = idx;
            texture.needsUpdate = true;
            layer.forceUpdate();
        }

        // 9.计算几何体摆放位置
        // target表示需要放置的物体
        function setGlobeTarget(globe, target, {latitude, longitude, transpose = false, ...attrs}) {
            // 球体的半径
            const radius = globe.attributes.radius;
            if(transpose) target.transpose();
            if(latitude != null && longitude != null) {
                const scale = target.attributes.scaleY * (attrs.scale || 1.0);
                const height = target.attributes.height;
                // 先用 projection 函数将经纬度映射为地图上的直角坐标，然后用直角坐标转球面坐标的公式
                const pos = latlngToPosition(latitude, longitude, radius);
                // 要将底部放置在地面上（根据缩放计算新的坐标位置）
                pos.scale(height * 0.5 * scale / radius + 1);
                attrs.pos = pos;
            }
            target.attr(attrs);
            const sp = new Vec3().copy(attrs.pos).scale(2);
            // 通过lootAt函数，让物体朝向球面的法向量方向（lookAt 函数是让物体的 z 轴朝向向量方向）
            target.lookAt(sp);
            globe.append(target);
        }

        /**
         * 将经纬度转换为球面坐标
         * @param {*} latitude
         * @param {*} longitude
         * @param {*} radius
         */
        function latlngToPosition(latitude, longitude, radius = 1) {
            const [u, v] = projection([longitude, latitude]);
            return project(u, v, radius);
        }

        /**
         * 将平面地图坐标转换为球面坐标
         * @param {*} u
         * @param {*} v
         * @param {*} radius
         */
        function project(u, v, radius = 1) {
            u /= mapScale * mapWidth;
            v /= mapScale * mapHeight;
            const pLength = Math.PI * 2;
            const tLength = Math.PI;
            const x = -radius * Math.cos(u * pLength) * Math.sin(v * tLength);
            const y = radius * Math.cos(v * tLength);
            const z = radius * Math.sin(u * pLength) * Math.sin(v * tLength);
            return new Vec3(x, y, z);
        }

        // 10.摆放光柱
        const beamVertx = `
            precision highp float;
            precision highp int;

            attribute vec3 position;
            attribute vec3 normal;
            attribute vec4 color;

            uniform mat4 modelViewMatrix;
            uniform mat4 projectionMatrix;
            uniform mat3 normalMatrix;

            varying vec3 vNormal;
            varying vec4 vColor;

            uniform vec4 ambientColor; // 环境光
            uniform float uHeight;

            void main() {
                vNormal = normalize(normalMatrix * normal);
                vec3 ambient = ambientColor.rgb * color.rgb;// 计算环境光反射颜色
                float height = 0.5 - position.z / uHeight;
                vColor = vec4(ambient + 0.3 * sin(height), color.a * height);
                vec3 P = position;
                P.xy *= 2.0 - pow(height, 3.0);
                gl_Position = projectionMatrix * modelViewMatrix * vec4(P, 1.0);
            }
        `;
        const beamFrag = `
            precision highp float;
            precision highp int;

            varying vec3 vNormal;
            varying vec4 vColor;

            void main() {
                gl_FragColor = vColor;
            }
        `
        
        function addBeam(globe, {
            latitude,
            longitude,
            width = 1.0,
            height = 25.0,
            color = 'rgba(245,250,113, 0.5)',
            raycast = 'none',
            segments = 60
        } = {}
        ) {
            const layer = globe.layer;
            const radius = globe.attributes.radius;
            if(layer) {
                const r = width / 2;
                const scale = radius * 0.015;
                const program = layer.createProgram({
                    transparent: true,
                    vertex: beamVertx,
                    fragment: beamFrag,
                    uniforms: {
                        uHeight: {value: height},
                    },
                });
                const beam = new Cylinder(program, {
                    radiusTop: r,
                    radiusBottom: r,
                    radialSegments: segments,
                    height,
                    colors: color,
                });
                setGlobeTarget(globe, beam, {transpose: true, latitude, longitude, scale, raycast});
                return beam;
            }
        }

        // 11.摆放地标
        


        // 开启旋转控制
        layer.setOrbit({autoRotate: false}); 
    </script>
</body>
</html>